AB#3312: collapse five Ensure* verbs behind BranchEnsurer#518
Merged
Conversation
Extract the duplicated idempotent-branch-ensure matrix from
BranchCommands.{EnsureFeature, EnsurePlan, EnsureImpl, EnsureMergeGroup,
EnsureEvidenceBranch} into a single shared Polyphony.Branching.BranchEnsurer.
Each Ensure* verb is now a thin shell that owns:
- input validation
- per-kind branch-name derivation
- per-kind `base-missing-on-remote` error hint
and delegates the (local-existed) x (remote-existed) x (base-existed) matrix
to the ensurer. Behaviour is preserved bit-for-bit at the JSON contract
surface; `artifacts/verb-output-schemas.json` diff vs. the locked fixture
is empty.
Design highlights (rubber-duck adopted):
- `BranchSpec` record carries Target, optional Base, Remote,
TolerateWorktreeConflict, IncludeBaseOnRemoteCheck.
- `BranchEnsureOutcome` uses private ctor + static factories
`Success(...)` / `BaseMissing(...)` so invalid states are
unrepresentable.
- Worktree-conflict tolerance preserved as feature-only (no behaviour
change in other kinds).
- `IncludeBaseOnRemoteCheck` gates BOTH the base-missing-on-remote
validation AND the `base_remote_existed` payload field, so Feature
(which has no base validation) cleanly opts out of both.
Helpers consolidated in BranchEnsurer:
- `TryGetCurrentBranchAsync`, `TryGetBranchShaAsync` moved from
BranchCommands.cs.
- `BaseExistsOnRemoteAsync` moved from EnsureMergeGroup.
- `BranchInOtherWorktreeRegex` moved from EnsureFeature.
Tests:
- 6 new direct matrix tests in
tests/Polyphony.Tests/Branching/BranchEnsurerTests.cs, including the
rubber-duck-pinned cases for tolerance ON + remote-absent (still
pushes, wasMutated=true) and tolerance OFF + conflict (propagates).
- 14 existing test fixtures updated to construct
`new Polyphony.Branching.BranchEnsurer(git)` as the 11th
BranchCommands ctor arg.
Validation:
- dotnet build (src + tests) clean.
- dotnet test full suite: 4012/4012 PASS.
- artifacts/verb-output-schemas.json diff vs. tests/lint/fixtures/...
is empty (pure refactor).
- Vocabulary + type-agnosticism lints clean.
Parent: AB#3311 (2026-05-24 architecture review).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Collapse the duplicated idempotent-branch-ensure matrix from the five
BranchCommands.Ensure*verbs (Feature / Plan / Impl / MergeGroup / EvidenceBranch) behind a single sharedPolyphony.Branching.BranchEnsurer. Pure refactor — JSON envelope unchanged on every verb;artifacts/verb-output-schemas.jsondiff vs. the locked fixture is empty.Parent: AB#3311 (2026-05-24 architecture review, item #2 — Five Ensure verbs duplicate idempotent-ensure matrix).
What moved
BranchCommands.Ensure{Feature,Plan,Impl,MergeGroup,EvidenceBranch}.cs— duplicated 8-arm matrixBranchEnsurer.EnsureAsync(BranchSpec)BranchCommands.cs—TryGetCurrentBranchAsync,TryGetBranchShaAsyncBranchEnsurer(the latter exposed; consumed by verbs to populatePayload.Sha)BranchCommands.EnsureMergeGroup.cs—BaseExistsOnRemoteAsyncBranchEnsurer(private)BranchCommands.EnsureFeature.cs—BranchInOtherWorktreeRegexBranchEnsurerEach
Ensure*verb still owns:plan/{root}-{child}vsimpl/{branchId}vsmg/...)base-missing-on-remoteerror hint (the hint depends on per-kind context the ensurer has no business knowing)…and delegates the matrix to the ensurer. The verb projects
BranchEnsureOutcomeinto its own kind-specific Result + Payload.Design highlights (rubber-duck adopted)
BranchSpecrecord:Target,Base?(string?),Remote,TolerateWorktreeConflict,IncludeBaseOnRemoteCheck.BranchEnsureOutcomeuses private ctor + static factories (Success(...)/BaseMissing(...)) so invalid-state combinations are unrepresentable.IncludeBaseOnRemoteCheckbecause it gates both the base-missing-on-remote validation and thebase_remote_existedpayload field. Feature (no base) sets it tofalseand cleanly opts out of both.ls-remotecall (forbase_remote_existedreporting) even when the target branch already exists, matching current observable behaviour.Visibility
BranchCommandsispublic(ConsoleAppFramework verb discovery), soBranchEnsurer+BranchSpec+BranchEnsureOutcome+BranchEnsureStatushad to bepublictoo (initialinternalattempt hit CS0051). They live inPolyphony.Branching.Test changes
tests/Polyphony.Tests/Branching/BranchEnsurerTests.cs— 6 focused matrix tests, including the rubber-duck-pinned cases:wasMutated=trueExternalToolExceptionpropagatesnew Polyphony.Branching.BranchEnsurer(git)as the 11thBranchCommandsctor arg (bulk + a few multi-line manual edits).Validation
dotnet build src/Polyphony/Polyphony.csproj -c Release✓dotnet build tests/Polyphony.Tests/Polyphony.Tests.csproj -c Release✓dotnet test tests/Polyphony.Tests/Polyphony.Tests.csproj -c Release --no-build→ 4012 / 4012 PASS ✓dotnet build src/Polyphony.SchemaExporter/Polyphony.SchemaExporter.csproj -c Releaseregeneratesartifacts/verb-output-schemas.json; diff vs.tests/lint/fixtures/verb-output-schemas.jsonis empty (pure refactor confirmed at the JSON contract surface).Files
src/Polyphony/Branching/BranchEnsurer.cstests/Polyphony.Tests/Branching/BranchEnsurerTests.cssrc/Polyphony/Commands/BranchCommands.cs+ the fiveEnsure*partialssrc/Polyphony/Infrastructure/PolyphonyServiceRegistration.cs(DI registration)tests/Polyphony.Tests/Commands/+tests/Polyphony.Tests/Journal/JournalE2ETests.csOut of scope